'use strict';

let React;
let ReactDOM;

let TestComponent;

describe('ReactCompositeComponent-state', () => {
    beforeEach(() => {
        React = require('react');
        ReactDOM = require('react-dom');

        TestComponent = class extends React.Component {
            constructor(props) {
                super(props);
                this.peekAtState('getInitialState', undefined, props);
                this.state = { color: 'red' };
            }

			peekAtState = (from, state = this.state, props = this.props) => {
			    props.stateListener(from, state && state.color);
			};

			peekAtCallback = from => {
			    return () => this.peekAtState(from);
			};

			setFavoriteColor(nextColor) {
			    this.setState({ color: nextColor }, this.peekAtCallback('setFavoriteColor'));
			}

			render() {
			    this.peekAtState('render');
			    return <div> {this.state.color} </div>;
			}

			UNSAFE_componentWillMount() {
			    this.peekAtState('componentWillMount-start');
			    this.setState(function(state) {
			        this.peekAtState('before-setState-sunrise', state);
			    });
			    this.setState({ color: 'sunrise' }, this.peekAtCallback('setState-sunrise'));
			    this.setState(function(state) {
			        this.peekAtState('after-setState-sunrise', state);
			    });
			    this.peekAtState('componentWillMount-after-sunrise');
			    this.setState({ color: 'orange' }, this.peekAtCallback('setState-orange'));
			    this.setState(function(state) {
			        this.peekAtState('after-setState-orange', state);
			    });
			    this.peekAtState('componentWillMount-end');
			}

			componentDidMount() {
			    this.peekAtState('componentDidMount-start');
			    this.setState({ color: 'yellow' }, this.peekAtCallback('setState-yellow'));
			    this.peekAtState('componentDidMount-end');
			}

			UNSAFE_componentWillReceiveProps(newProps) {
			    this.peekAtState('componentWillReceiveProps-start');
			    if (newProps.nextColor) {
			        this.setState(function(state) {
			            this.peekAtState('before-setState-receiveProps', state);
			            return { color: newProps.nextColor };
			        });
			        // No longer a public API, but we can test that it works internally by
			        // reaching into the updater.
			        //  this.updater.enqueueReplaceState(this, {color: undefined});
			        this.setState(function(state) {
			            this.peekAtState('before-setState-again-receiveProps', state);
			            return { color: newProps.nextColor };
			        }, this.peekAtCallback('setState-receiveProps'));
			        this.setState(function(state) {
			            this.peekAtState('after-setState-receiveProps', state);
			        });
			    }
			    this.peekAtState('componentWillReceiveProps-end');
			}

			shouldComponentUpdate(nextProps, nextState) {
			    this.peekAtState('shouldComponentUpdate-currentState');
			    this.peekAtState('shouldComponentUpdate-nextState', nextState);
			    return true;
			}

			UNSAFE_componentWillUpdate(nextProps, nextState) {
			    this.peekAtState('componentWillUpdate-currentState');
			    this.peekAtState('componentWillUpdate-nextState', nextState);
			}

			componentDidUpdate(prevProps, prevState) {
			    this.peekAtState('componentDidUpdate-currentState');
			    this.peekAtState('componentDidUpdate-prevState', prevState);
			}

			componentWillUnmount() {
			    this.peekAtState('componentWillUnmount');
			}
        };
    });

    it('should support setting state', () => {
        const container = document.createElement('div');
        document.body.appendChild(container);

        const stateListener = jest.fn();
        const instance = ReactDOM.render(
            <TestComponent stateListener={stateListener} />,
            container,
            function peekAtInitialCallback() {
                this.peekAtState('initial-callback');
            }
        );
        ReactDOM.render(
            <TestComponent stateListener={stateListener} nextColor="green" />,
            container,
            instance.peekAtCallback('setProps')
        );
        instance.setFavoriteColor('blue');
        instance.forceUpdate(instance.peekAtCallback('forceUpdate'));

        ReactDOM.unmountComponentAtNode(container);

        let expected = [
            // there is no state when getInitialState() is called
            ['getInitialState', null],
            ['componentWillMount-start', 'red'],
            // setState()'s only enqueue pending states.
            ['componentWillMount-after-sunrise', 'red'],
            ['componentWillMount-end', 'red'],
            // pending state queue is processed
            ['before-setState-sunrise', 'red'],
            ['after-setState-sunrise', 'sunrise'],
            ['after-setState-orange', 'orange'],
            // pending state has been applied
            ['render', 'orange'],
            ['componentDidMount-start', 'orange'],
            // setState-sunrise and setState-orange should be called here,
            // after the bug in #1740
            // componentDidMount() called setState({color:'yellow'}), which is async.
            // The update doesn't happen until the next flush.
            ['componentDidMount-end', 'orange'],
            ['setState-sunrise', 'orange'],
            ['setState-orange', 'orange'],
            ['initial-callback', 'orange'],
            ['shouldComponentUpdate-currentState', 'orange'],
            ['shouldComponentUpdate-nextState', 'yellow'],
            ['componentWillUpdate-currentState', 'orange'],
            ['componentWillUpdate-nextState', 'yellow'],
            ['render', 'yellow'],
            ['componentDidUpdate-currentState', 'yellow'],
            ['componentDidUpdate-prevState', 'orange'],
            //** ['componentDidUpdate-prevState', 'yellow'],
            ['setState-yellow', 'yellow'],
            ['componentWillReceiveProps-start', 'yellow'],
            // setState({color:'green'}) only enqueues a pending state.
            ['componentWillReceiveProps-end', 'yellow'],
            // pending state queue is processed
            // We keep updates in the queue to support
            // replaceState(prevState => newState).
            ['before-setState-receiveProps', 'yellow'],
            ['before-setState-again-receiveProps', 'green'],
            ['after-setState-receiveProps', 'green'],
            ['shouldComponentUpdate-currentState', 'yellow'],
            ['shouldComponentUpdate-nextState', 'green'],
            ['componentWillUpdate-currentState', 'yellow'],
            ['componentWillUpdate-nextState', 'green'],
            ['render', 'green'],
            ['componentDidUpdate-currentState', 'green'],
            ['componentDidUpdate-prevState', 'yellow'],
            ['setState-receiveProps', 'green'],
            ['setProps', 'green'],
            // setFavoriteColor('blue')
            ['shouldComponentUpdate-currentState', 'green'],
            ['shouldComponentUpdate-nextState', 'blue'],
            ['componentWillUpdate-currentState', 'green'],
            ['componentWillUpdate-nextState', 'blue'],
            ['render', 'blue'],
            ['componentDidUpdate-currentState', 'blue'],
            //** ['componentDidUpdate-prevState', 'yellow'],
            ['componentDidUpdate-prevState', 'green'],
            ['setFavoriteColor', 'blue'],
            // forceUpdate()
            ['componentWillUpdate-currentState', 'blue'],
            ['componentWillUpdate-nextState', 'blue'],
            ['render', 'blue'],
            ['componentDidUpdate-currentState', 'blue'],
            ['componentDidUpdate-prevState', 'blue'],
            //**  ['componentDidUpdate-prevState', 'yellow'],
            ['forceUpdate', 'blue'],
            // unmountComponent()
            // state is available within `componentWillUnmount()`
            ['componentWillUnmount', 'blue']
        ];

        expect(stateListener.mock.calls.join('\n')).toEqual(expected.join('\n'));
    });

    it('should call componentDidUpdate of children first', () => {
        // "暂不测试unstable API"

        const container = document.createElement('div');

        let ops = [];

        let child = null;
        let parent = null;

        class Child extends React.Component {
			state = { bar: false };
			componentDidMount() {
			    child = this;
			}
			componentDidUpdate() {
			    ops.push('child did update');
			}
			render() {
			    return <div />;
			}
        }

        let shouldUpdate = true;

        class Intermediate extends React.Component {
            shouldComponentUpdate() {
                return shouldUpdate;
            }
            render() {
                return <Child />;
            }
        }

        class Parent extends React.Component {
			state = { foo: false };
			componentDidMount() {
			    parent = this;
			}
			componentDidUpdate() {
			    ops.push('parent did update');
			}
			render() {
			    return <Intermediate />;
			}
        }

        ReactDOM.render(<Parent />, container);

        ReactDOM.unstable_batchedUpdates(() => {
            parent.setState({ foo: true });
            child.setState({ bar: true });
        });
        // When we render changes top-down in a batch, children's componentDidUpdate
        // happens before the parent.
        expect(ops).toEqual(['child did update', 'parent did update']);

        shouldUpdate = false;

        ops = [];

        ReactDOM.unstable_batchedUpdates(() => {
            parent.setState({ foo: false });
            child.setState({ bar: false });
        });
        // We expect the same thing to happen if we bail out in the middle.
        expect(ops).toEqual(['child did update', 'parent did update']);
    });

    it('should batch unmounts', () => {
        let outer;

        class Inner extends React.Component {
            render() {
                return <div />;
            }

            componentWillUnmount() {
                // This should get silently ignored (maybe with a warning), but it
                // shouldn't break React.
                outer.setState({ showInner: false });
            }
        }

        class Outer extends React.Component {
			state = { showInner: true };

			render() {
			    return <div> {this.state.showInner && <Inner />} </div>;
			}
        }

        const container = document.createElement('div');
        outer = ReactDOM.render(<Outer />, container);
        expect(() => {
            ReactDOM.unmountComponentAtNode(container);
        }).not.toThrow();
    });

    it('should update state when called from child cWRP', function() {
        const log = [];
        class Parent extends React.Component {
			state = { value: 'one' };
			render() {
			    log.push('parent render ' + this.state.value);
			    return <Child parent={this} value={this.state.value} />;
			}
        }
        let updated = false;
        class Child extends React.Component {
            UNSAFE_componentWillReceiveProps() {
                if (updated) {
                    return;
                }
                log.push('child componentWillReceiveProps ' + this.props.value);
                this.props.parent.setState({ value: 'two' });
                log.push('child componentWillReceiveProps done ' + this.props.value);
                updated = true;
            }
            render() {
                log.push('child render ' + this.props.value);
                return <div> {this.props.value} </div>;
            }
        }
        const container = document.createElement('div');
        ReactDOM.render(<Parent />, container);
        ReactDOM.render(<Parent />, container);
        expect(log).toEqual([
            'parent render one',
            'child render one',
            'parent render one',
            'child componentWillReceiveProps one',
            'child componentWillReceiveProps done one',
            'child render one',
            'parent render two',
            'child render two'
        ]);
    });

    it('should merge state when sCU returns false', function() {
        const log = [];
        class Test extends React.Component {
			state = { a: 0 };
			render() {
			    return null;
			}
			shouldComponentUpdate(nextProps, nextState) {
			    log.push('scu from ' + Object.keys(this.state) + ' to ' + Object.keys(nextState));
			    return false;
			}
        }

        const container = document.createElement('div');
        const test = ReactDOM.render(<Test />, container);
        test.setState({ b: 0 });
        expect(log.length).toBe(1);
        test.setState({ c: 0 });
        expect(log.length).toBe(2);
        expect(log).toEqual(['scu from a to a,b', 'scu from a,b to a,b,c']);
    });

    it('should treat assigning to this.state inside cWRP as a replaceState, with a warning', () => {
        let ops = [];
        class Test extends React.Component {
			state = { step: 1, extra: true };
			UNSAFE_componentWillReceiveProps() {
			    this.setState({ step: 2 }, () => {
			        // Tests that earlier setState callbacks are not dropped
			        ops.push(`callback -- step: ${this.state.step}, extra: ${!!this.state.extra}`);
			    });
			    // Treat like replaceState
			    this.state = { step: 3 };
			}
			render() {
			    ops.push(`render -- step: ${this.state.step}, extra: ${!!this.state.extra}`);
			    return null;
			}
        }

        // Mount
        const container = document.createElement('div');
        ReactDOM.render(<Test />, container);
        // Update
        expect(() => ReactDOM.render(<Test />, container)).toWarnDev(
            'Warning: Test.componentWillReceiveProps(): Assigning directly to ' +
				'this.state is deprecated (except inside a component\'s constructor). ' +
				'Use setState instead.'
        );

        expect(ops).toEqual([
            'render -- step: 1, extra: true',
            'render -- step: 2, extra: false',
            'callback -- step: 2, extra: false'
        ]);

        // Check deduplication; (no additional warnings are expected)
        ReactDOM.render(<Test />, container);
    });

    it('should treat assigning to this.state inside cWM as a replaceState, with a warning', () => {
        let ops = [];
        class Test extends React.Component {
			state = { step: 1, extra: true };
			UNSAFE_componentWillMount() {
			    this.setState({ step: 2 }, () => {
			        // Tests that earlier setState callbacks are not dropped
			        ops.push(`callback -- step: ${this.state.step}, extra: ${!!this.state.extra}`);
			    });
			    // Treat like replaceState
			    this.state = { step: 3 };
			}
			render() {
			    ops.push(`render -- step: ${this.state.step}, extra: ${!!this.state.extra}`);
			    return null;
			}
        }

        // Mount
        const container = document.createElement('div');
        expect(() => ReactDOM.render(<Test />, container)).toWarnDev(
            'Warning: Test.componentWillMount(): Assigning directly to ' +
				'this.state is deprecated (except inside a component\'s constructor). ' +
				'Use setState instead.'
        );

        expect(ops).toEqual(['render -- step: 2, extra: false', 'callback -- step: 2, extra: false']);
    });

    it('should support stateful module pattern components', () => {});

    it('should support getDerivedStateFromProps for module pattern components', () => {});
});
